package org.gap.wizards.fov;

import java.awt.Dimension;
import java.awt.Point;
import java.util.ArrayList;
import java.util.List;

import org.gap.wizards.world.Location;

public class ShadowCasting {
	private final Point player;
	private List<Point> sight;
	private final boolean[][] isBlocked;
	int visualRange = 10;

	public ShadowCasting(Location location) {
		player = location.getCharacterLocation();
		Dimension dimensions = location.getDimensions();
		isBlocked = new boolean[dimensions.width][dimensions.height];
		for (int x = 0; x < dimensions.width; x++) {
			for (int y = 0; y < dimensions.height; y++) {
				isBlocked[x][y] = location.isBlocked(new Point(x, y));
			}
		}
	}

	public List<Point> getVisibleArea() {
		sight = new ArrayList<Point>();

		for (int octant = 1; octant < 9; octant++) {
			scan(1, octant, 1.0, 0.0);
		}
		return sight;
	}

	public void scan(int depth, int octant, double startSlope, double endSlope) {
		int x = 0;
		int y = 0;
		switch (octant) {
			case 1:
				y = player.y - depth;
				x = player.x - multiply(depth, startSlope);

				if (!isPointOnMap(x, y)) {
					break;
				}

				while (getSlope(x, y, player.x, player.y) >= endSlope) {
					if (withinVisualRange(x, y)) {
						if (isBlocked(x, y)) {
							if (isOpen(x - 1, y)) { // if prior open AND within range
								scan(depth + 1, octant, startSlope, getSlope(x - 0.5, y + 0.5, player.x, player.y));
							}
						} else {
							if (isClosed(x - 1, y)) { // if prior closed AND within range
								startSlope = getSlope(x - 0.5f, y - 0.5f, player.x, player.y);
							}
							sight.add(new Point(x, y));
						}
					}
					x++;
				}
				x--; // we step back as the last step of the while has taken us past the limit
				break;
			case 2:
				y = player.y - depth;
				x = player.x + multiply(depth, startSlope);

				if (!isPointOnMap(x, y)) {
					break;
				}

				while (getSlope(x, y, player.x, player.y) <= endSlope) {
					if (withinVisualRange(x, y)) {
						if (isBlocked(x, y)) {
							if (isOpen(x + 1, y)) {
								scan(depth + 1, octant, startSlope, getSlope(x + 0.5, y + 0.5, player.x, player.y));
							}
						} else {
							if (isClosed(x + 1, y)) {
								startSlope = -getSlope(x + 0.5, y - 0.5, player.x, player.y);
							}
							sight.add(new Point(x, y));
						}
					}
					x--;
				}
				x++;
				break;
			case 3:
				x = player.x + depth;
				y = player.y - multiply(depth, startSlope);

				if (!isPointOnMap(x, y)) {
					break;
				}

				while (getSlopeInv(x, y, player.x, player.y) <= endSlope) {
					if (withinVisualRange(x, y)) {
						if (isBlocked(x, y)) {
							if (isOpen(x, y - 1)) {
								scan(depth + 1, octant, startSlope, getSlopeInv(x - 0.5, y - 0.5, player.x, player.y));
							}
						} else {
							if (isClosed(x, y - 1)) {
								startSlope = -getSlopeInv(x + 0.5, y - 0.5, player.x, player.y);
							}
							sight.add(new Point(x, y));
						}
					}
					y++;
				}
				y--; // we step back as the last step of the while has taken us
						// past the limit
				break;
			case 4:
				x = player.x + depth;
				y = player.y + multiply(depth, startSlope);

				if (!isPointOnMap(x, y)) {
					break;
				}

				while (getSlopeInv(x, y, player.x, player.y) >= endSlope) {
					if (withinVisualRange(x, y)) {
						if (isBlocked(x, y)) {
							if (isOpen(x, y + 1)) {
								scan(depth + 1, octant, startSlope, getSlopeInv(x - 0.5, y + 0.5, player.x, player.y));
							}
						} else {
							if (isClosed(x, y + 1)) {
								startSlope = getSlopeInv(x + 0.5, y + 0.5, player.x, player.y);
							}
							sight.add(new Point(x, y));
						}
					}
					y--;
				}
				y++;
				break;
			case 5:
				y = player.y + depth;
				x = player.x + multiply(depth, startSlope);

				if (!isPointOnMap(x, y)) {
					break;
				}

				while (getSlope(x, y, player.x, player.y) >= endSlope) {
					if (withinVisualRange(x, y)) {
						if (isBlocked(x, y)) {
							if (isOpen(x + 1, y)) {
								scan(depth + 1, octant, startSlope, getSlope(x + 0.5, y - 0.5, player.x, player.y));
							}
						} else {
							if (isClosed(x + 1, y)) {
								startSlope = getSlope(x + 0.5, y + 0.5, player.x, player.y);
							}
							sight.add(new Point(x, y));
						}
					}
					x--;
				}
				x++;
				break;
			case 6:
				y = player.y + depth;
				x = player.x - multiply(depth, startSlope);

				if (!isPointOnMap(x, y)) {
					break;
				}

				while (getSlope(x, y, player.x, player.y) <= endSlope) {
					if (withinVisualRange(x, y)) {
						if (isBlocked(x, y)) {
							if (isOpen(x - 1, y)) {
								scan(depth + 1, octant, startSlope, getSlope(x - 0.5, y - 0.5, player.x, player.y));
							}
						} else {
							if (isClosed(x - 1, y)) {
								startSlope = -getSlope(x - 0.5, y + 0.5, player.x, player.y);
							}
							sight.add(new Point(x, y));
						}
					}
					x++;
				}
				x--;
				break;
			case 7:
				x = player.x - depth;
				y = player.y + multiply(depth, startSlope);

				if (!isPointOnMap(x, y)) {
					break;
				}

				while (getSlopeInv(x, y, player.x, player.y) <= endSlope) {
					if (withinVisualRange(x, y)) {
						if (isBlocked(x, y)) {
							if (isOpen(x, y + 1)) {
								scan(depth + 1, octant, startSlope, getSlopeInv(x + 0.5, y + 0.5, player.x, player.y));
							}
						} else {
							if (isClosed(x, y + 1)) {
								startSlope = getSlopeInv(x - 0.5, y + 0.5, player.x, player.y);
							}
							sight.add(new Point(x, y));
						}
					}
					y--;
				}
				y++;
				break;
			case 8:
				x = player.x - depth;
				y = player.y - multiply(depth, startSlope);

				if (!isPointOnMap(x, y)) {
					break;
				}

				while (getSlopeInv(x, y, player.x, player.y) >= endSlope) {
					if (withinVisualRange(x, y)) {
						if (isBlocked(x, y)) {
							if (isOpen(x, y - 1)) {
								scan(depth + 1, octant, startSlope, getSlopeInv(x + 0.5, y - 0.5, player.x, player.y));
							}
						} else {
							if (isClosed(x, y - 1)) {
								startSlope = getSlopeInv(x - 0.5, y - 0.5, player.x, player.y);
							}
							sight.add(new Point(x, y));
						}
					}
					y++;
				}
				y--;
				break;
		}

		if (withinVisualRange(x, y) && !isBlocked[x][y]) {
			scan(depth + 1, octant, startSlope, endSlope);
		}
	}

	private double getSlope(double x1, double y1, double x2, double y2) {
		return (x1 - x2) / (y1 - y2);
	}

	private double getSlopeInv(double x1, double y1, double x2, double y2) {
		return (y1 - y2) / (x1 - x2);
	}


	private boolean isBlocked(int x, int y) {
		return isBlocked[x][y];
	}

	private int multiply(int depth, double startSlope) {
		return (int) ((startSlope * (double) (depth)));
	}

	private boolean isOpen(int x, int y) {
		try {
			if (!withinVisualRange(x, y)) {
				return false;
			}
			return !isBlocked[x][y];
		} catch (Exception e) {
			return false;
		}
	}
	
	private boolean isClosed(int x, int y) {
		try {
			if (!withinVisualRange(x, y)) {
				return false;
			}
			return isBlocked[x][y];
		} catch (Exception e) {
			return false;
		}
	}

	protected boolean withinVisualRange(int x, int y) {
		if (isPointOnMap(x, y)) {
			return player.distance(new Point(x, y)) <= visualRange;
		}
		return false;
	}
	
	private boolean isPointOnMap(int x, int y) {
		if (x < 0) return false;
		if (x > isBlocked.length - 1) return false;
		if (y < 0) return false;
		if (y > isBlocked[0].length - 1) return false;
		
		return true;
	}
}
